package idp.communication;

import idp.dashboard.MainFrame;
import idp.exceptions.BluetoothConnectionException;
import idp.exceptions.UserCanceledActionException;
import idp.settings.SettingModel;
import idp.utils.UserInput;

import java.io.IOException;
import java.util.Vector;
import javax.bluetooth.*;
import javax.microedition.io.Connection;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.swing.ProgressMonitor;

import com.intel.bluetooth.RemoteDeviceHelper;

/**
 * Setups up and creates the bluetooth connection. Has methodes to search for a
 * device and connect to it. This class is a singleton.
 * 
 * @author R. Schaafsma
 * 
 */
public class BluetoothConnection {

	// serial port protocol service id, used when doing a service search
	private static UUID[] serviceUUID = { new UUID(0x1101) };

	// spp attribute id. Used in a service search to identify the channel of the
	// spp
	private static int[] attrUUID = { 0x0003 };

	// reference to the current instance of BluetoothConnection
	private static BluetoothConnection _instance;

	private String _url = null;

	/**
	 * Constructor. Not to be called directly, use getInstance() to access this
	 * class
	 * 
	 * @see #getInstance()
	 */
	private BluetoothConnection() {
		_instance = this;
	}

	/**
	 * Returns the current instance of BluetoothConnection. If there isn't an
	 * instance, a new instance will be created.
	 * 
	 * @return the current BluetoothConnection instance
	 */
	public static BluetoothConnection getInstance() {
		if (_instance == null) {
			new BluetoothConnection();
		}
		return _instance;
	}

	/**
	 * private inner class used for storing the bluetooth devices found during a
	 * search. It's basically a wrapper around the RemoteDevice class, friendly
	 * name and bluetooth address.
	 * 
	 * @author R. Schaafsma
	 * 
	 */
	private static class btDevice {
		private RemoteDevice _device;

		private String _friendlyName;

		private String _address;

		/**
		 * Constructor
		 * 
		 * @param device
		 *            The RemoteDevice object to wrap around
		 * @see RemoteDevice
		 */
		public btDevice(RemoteDevice device) {
			_device = device;
			_address = _device.getBluetoothAddress();
		}

		/**
		 * Retrieves the friendly name of the remote device.
		 */
		public void getFriendlyName() {
			try {
				_friendlyName = _device.getFriendlyName(false);
			} catch (IOException e) {
				System.err.println("Unable to get friendly name for "
						+ _device.getBluetoothAddress());
			}
		}

		/**
		 * returns the RemoveDevice object
		 * 
		 * @return the remote device
		 * @see RemoteDevice
		 */
		public RemoteDevice getDevice() {			
			return _device;
		}

		/**
		 * Override of toString. Will return a formatted string of the address
		 * and, if available, the friendly name.
		 */
		public String toString() {
			if (_friendlyName != null) {
				return _friendlyName + " (" + _address + ")";
			} else {
				return _address;
			}
		}
	}

	/**
	 * Finds the correct device to connect to. Will retrieve the device url from
	 * the settings model if it's known there. If not if will do a device search
	 * and allow the user to choose the correct device.
	 * 
	 * @throws UserCanceledActionException
	 * @see {@link UserInput#getUserSelection(String, String[])}
	 */
	public void choseDevice() throws UserCanceledActionException {
		// check for url in the settings model
		SettingModel sm = idp.settings.SettingModel.LoadByKey("BT_URL");
		RemoteDevice BTDevice = null;
		if (sm == null || sm.getValue() == null || sm.getValue().equals("")) {
			// not in settings model, do a search and request user to select a
			// device.
			btDevice[] devices = getRemoteDevices();
			btDevice d = ((btDevice) idp.utils.UserInput.getUserSelection(
					"Kies een apparaat waarmee u wilt verbinden", devices));
			if (d == null) {
				// No device selected, throw exception
				throw new UserCanceledActionException(
						"User canceled the selection of a device");
				// System.exit(0);
			} else {
				BTDevice = d.getDevice();
			}
			_url = getConnectionURL(BTDevice);

			// ask if the user wants to save the current device in the settings
			// model.
			if (idp.utils.UserInput
					.getUserYesNO("Wilt u Deze keuze opslaan in de instellingen?")) {
				if (sm == null) {
					SettingModel.CreateModel("BT_URL");
				}
				SettingModel.LoadByKey("BT_URL").setValue(_url);

			}
		} else if (sm != null) {
			_url = sm.getValue();
		}
	}

	/**
	 * connects to the remote device. Must be called after choseDevice(),
	 * otherwise there will be no device to connect to.
	 * @return a streamConnection to the bluetooth device
	 * @throws BluetoothConnectionException
	 * @see #choseDevice
	 */
	public StreamConnection connect() throws BluetoothConnectionException {
		try {
			return (StreamConnection) Connector.open(_url);			
		} catch (IOException e) {			
			throw new BluetoothConnectionException("Kan niet verbinden met bluetooth apparaat");
		}
	}

	/**
	 * Does a bluetooth search for nearby remote devices. 
	 * @return an array of all nearby bluetooth devices
	 */
	public btDevice[] getRemoteDevices() {
		//loading popup
		final ProgressMonitor progress = new ProgressMonitor(MainFrame
				.getInstance(), "Zoeken naar bluetooth apparaten", "", 0, 100);
		progress.setProgress(0);
		progress.setMillisToPopup(0);
		progress.setMillisToDecideToPopup(0);
		//vector which contains all the devices discovered so far.
		final Vector<btDevice> devicesDiscovered = new Vector<btDevice>();
		//object to synchronized on
		final Object completed = new Object();
		//discoveryListener which gets called everytime a device gets called.
		DiscoveryListener listener = new DiscoveryListener() {

			public void servicesDiscovered(int arg0, ServiceRecord[] arg1) {
				// TODO Auto-generated method stub

			}

			public void serviceSearchCompleted(int arg0, int arg1) {
				// TODO Auto-generated method stub

			}

			public void inquiryCompleted(int arg0) {
				progress.setNote("Namen verkrijgen");
				progress.setProgress(66);
//				if (progress.isCanceled()){					
//					System.exit(0);	
//				}					
				synchronized (completed) {
					//search is done, wake up the main thread
					completed.notifyAll();					
				}
			}

			/**
			 * Called when a new device is found.
			 */
			public void deviceDiscovered(RemoteDevice btdev, DeviceClass btclass) {
				progress.setProgress(13);
				devicesDiscovered.add(new btDevice(btdev));
			}

		};

		synchronized (completed) {
			boolean started = false;

			try {
				//start the device search
				started = LocalDevice.getLocalDevice().getDiscoveryAgent()
						.startInquiry(DiscoveryAgent.GIAC, listener);
			} catch (BluetoothStateException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if (started) {
				progress.setNote("Scannen voor apperaten");
				progress.setProgress(33);
				try {
					//wait for the device search to complete
					completed.wait();
				} catch (InterruptedException e) {
				}
				//get the friendly names
				for (btDevice b : devicesDiscovered) {
					b.getFriendlyName();
				}
			}
		}
		progress.setProgress(progress.getMaximum());

		//return the array of all found devices.
		return devicesDiscovered.toArray(new btDevice[1]);
	}

	/**
	 * Gets the connection url for the specified device. 
	 * @param device device to get connection url for
	 * @return the connection url
	 */
	private String getConnectionURL(RemoteDevice device) {
		//object to lock on
		final Object completed = new Object();
		final Vector<String> urls = new Vector<String>();
		//discovery listener called whenever the desired service is discovered.
		DiscoveryListener listener = new DiscoveryListener() {

			public void deviceDiscovered(RemoteDevice arg0, DeviceClass arg1) {
				// TODO Auto-generated method stub

			}

			public void inquiryCompleted(int arg0) {
				// TODO Auto-generated method stub

			}

			public void serviceSearchCompleted(int id, int arg1) {
				synchronized (completed) {
					completed.notifyAll();
				}

			}

			public void servicesDiscovered(int id, ServiceRecord[] serviceRecord) {
				for (int i = 0; i < serviceRecord.length; i++) {
					urls.add(serviceRecord[i].getConnectionURL(
							ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false));
				}

			}

		};
		synchronized (completed) {
			try {
				//start service search
				LocalDevice
						.getLocalDevice()
						.getDiscoveryAgent()
						.searchServices(attrUUID, serviceUUID, device, listener);
			} catch (BluetoothStateException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			try {
				//wait till the search is complete
				completed.wait();
			} catch (InterruptedException e) {
			}
		}
		if (urls.size() > 0) {
			System.out.println(urls.get(0));
			return urls.get(0);
		} else {
			return "ERROR - url not found";
		}
	}
}
